<!DOCTYPE html>
<html lang="en">
  <head>
    <title>three.js webgl - additive animation - skinning</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
    />
    <link type="text/css" rel="stylesheet" href="./main.css" />
    <style>
      a {
        color: blue;
      }

      .control-inactive button {
        color: #888;
      }
    </style>
  </head>

  <body>
    <div id="container"></div>
    <script type="importmap">
      {
        "imports": {
          "three": "https://esm.sh/three@0.174.0/build/three.module.js",
          "three/addons/": "https://esm.sh/three@0.174.0/examples/jsm/"
        }
      }
    </script>

    <script type="module">
      import * as THREE from "three";

      import { OrbitControls } from "three/addons/controls/OrbitControls.js";

      import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

      import { Octree } from "three/addons/math/Octree.js"; // 导入八叉树，用于高效的碰撞检测

      import { Capsule } from "three/addons/math/Capsule.js"; // 导入胶囊体，用于表示玩家的碰撞体积

      import { GUI } from "three/addons/libs/lil-gui.module.min.js";

      import Stats from "three/addons/libs/stats.module.js";

      const clock = new THREE.Clock();

      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0x88ccee);
      scene.fog = new THREE.Fog(0x88ccee, 0, 50);

      const camera = new THREE.PerspectiveCamera(
        70,
        window.innerWidth / window.innerHeight,
        0.1,
        10000
      );
      camera.rotation.order = "YXZ";
      camera.position.set(0, 2, 5); // 设置初始相机位置在角色后方上方

      const fillLight1 = new THREE.HemisphereLight(0x4488bb, 0x002244, 2.5);
      fillLight1.position.set(2, 1, 1);
      // scene.add(fillLight1);

      const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
      directionalLight.position.set(-5, 25, -1);
      directionalLight.castShadow = true;
      directionalLight.shadow.camera.near = 0.01;
      directionalLight.shadow.camera.far = 500;
      directionalLight.shadow.camera.right = 30;
      directionalLight.shadow.camera.left = -30;
      directionalLight.shadow.camera.top = 30;
      directionalLight.shadow.camera.bottom = -30;
      directionalLight.shadow.mapSize.width = 1024;
      directionalLight.shadow.mapSize.height = 1024;
      directionalLight.shadow.radius = 4;
      directionalLight.shadow.bias = -0.00006;
      scene.add(directionalLight);

      const container = document.getElementById("container");

      const renderer = new THREE.WebGLRenderer({
        antialias: true,
      });
      renderer.setPixelRatio(window.devicePixelRatio);
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.shadowMap.enabled = true;
      renderer.shadowMap.type = THREE.VSMShadowMap;
      renderer.outputEncoding = THREE.sRGBEncoding;
      renderer.toneMapping = THREE.ACESFilmicToneMapping;
      container.appendChild(renderer.domElement);

      var oControls = new OrbitControls(camera, container);
      oControls.enableDamping = true; // 启用阻尼效果，使相机移动更平滑
      oControls.dampingFactor = 0.05;
      oControls.minDistance = 3; // 设置最小距离，防止相机太靠近角色
      oControls.maxDistance = 10; // 设置最大距离
      oControls.update();

      const stats = new Stats();
      stats.domElement.style.position = "absolute";
      stats.domElement.style.top = "0px";
      container.appendChild(stats.domElement);

      scene.add(new THREE.AmbientLight(0xe0ffff, 1));

      // scene.add(new THREE.HemisphereLight("#ffffff", "#6b6b6b", 2));

      const geometry = new THREE.CapsuleGeometry(0.35, 0.7, 4, 8);
      const material = new THREE.MeshBasicMaterial({
        color: 0x00ff00,
        wireframe: true,
      });
      const capsule = new THREE.Mesh(geometry, material);
      // scene.add(capsule);

      const GRAVITY = 30;

      const NUM_SPHERES = 100;
      const SPHERE_RADIUS = 0.2;

      const STEPS_PER_FRAME = 5;

      const sphereGeometry = new THREE.IcosahedronGeometry(SPHERE_RADIUS, 5);
      const sphereMaterial = new THREE.MeshLambertMaterial({
        color: 0xbbbb44,
      });

      let peopleObj, activeAction, peopleAnimations, tween, mixer, peopleBox;
      let mixerArr = [];
      let currentAction = "idle";
      let idleAction, walkAction;
      const spheres = [];
      let sphereIdx = 0;

      const worldOctree = new Octree(); // 创建一个八叉树实例，用于存储世界中的静态几何体，以便快速进行碰撞查询

      // 创建一个胶囊体实例，作为玩家的物理碰撞体
      // 参数：起点向量，终点向量，半径
      const playerCollider = new Capsule(
        new THREE.Vector3(0, 0.35, 0), // 胶囊体底部球心
        new THREE.Vector3(0, 1, 0),    // 胶囊体顶部球心
        0.26 // 胶囊体半径
      );

      const playerVelocity = new THREE.Vector3(); // 玩家当前速度向量
      const playerDirection = new THREE.Vector3(); // 玩家当前朝向向量

      let playerOnFloor = false; // 标记玩家是否在地面上
      let mouseTime = 0;

      const keyStates = {};

      const vector1 = new THREE.Vector3();
      const vector2 = new THREE.Vector3();
      const vector3 = new THREE.Vector3();

      document.addEventListener("keydown", (event) => {
        keyStates[event.code] = true;
      });

      document.addEventListener("keyup", (event) => {
        keyStates[event.code] = false;
      });

      window.addEventListener("resize", onWindowResize);

      function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

        renderer.setSize(window.innerWidth, window.innerHeight);
      }

      // 处理玩家与世界环境的碰撞
      function playerCollisions() {
        // 使用八叉树检测玩家胶囊体与世界几何体的碰撞
        // capsuleIntersect 返回最近的碰撞信息，如果没有碰撞则返回 null
        const result = worldOctree.capsuleIntersect(playerCollider);

        playerOnFloor = false; // 先假设玩家不在地面上

        if (result) {
          // 如果发生碰撞 (result 不为 null)
          // 检查碰撞法线的 Y 分量，如果大于 0，说明碰撞发生在下方，即玩家在地面上
          playerOnFloor = result.normal.y > 0;

          if (!playerOnFloor) {
            // 如果碰撞不是来自下方（例如撞墙或天花板）
            // 调整玩家速度，使其沿碰撞表面滑动，而不是穿透
            // 通过将速度向量投影到碰撞法线的反方向，并减去该投影分量来实现
            playerVelocity.addScaledVector(
              result.normal,
              -result.normal.dot(playerVelocity)
            );
          }

          // 将玩家胶囊体沿碰撞法线方向移出碰撞物，移动距离为碰撞深度
          // 这是为了修正位置，防止穿透
          playerCollider.translate(result.normal.multiplyScalar(result.depth));
        }
      }

      // 更新玩家状态（位置、速度等）
      function updatePlayer(deltaTime) {
        // 计算阻尼系数，模拟空气阻力或摩擦力
        let damping = Math.exp(-4 * deltaTime) - 1;

        if (!playerOnFloor) {
          // 如果玩家不在地面上，施加重力
          playerVelocity.y -= GRAVITY * deltaTime;

          // 空中时阻力减小
          damping *= 0.1;
        }

        // 将阻尼应用到速度上
        playerVelocity.addScaledVector(playerVelocity, damping);

        // 根据当前速度计算这一帧玩家应该移动的位移
        const deltaPosition = playerVelocity.clone().multiplyScalar(deltaTime);
        // 移动玩家的物理碰撞体（胶囊体）
        playerCollider.translate(deltaPosition);

        // 对移动后的胶囊体进行碰撞检测和处理
        playerCollisions();

        // 获取碰撞处理后胶囊体的最终位置
        let resPos = playerCollider.end;
        // 更新可视化胶囊体模型的位置（如果场景中添加了它）
        capsule.position.copy(resPos);

        if (peopleObj) {
          // 如果人物模型已加载
          // 更新人物模型的位置，使其底部与碰撞胶囊体的底部对齐
          peopleObj.position.copy(resPos.clone().setY(resPos.y - 0.7));

          // 设置相机跟随目标点为角色位置 (略微抬高)
          let pos = peopleObj.position.clone();
          pos = pos.setY(pos.y + 0.7);
          oControls.target = pos;

          // 计算相机理想位置：在角色后方一定距离
          const idealOffset = new THREE.Vector3(0, 1.5, 4); // 相对于角色的偏移量
          const idealLookAt = new THREE.Vector3(0, 0.5, 0); // 相对于角色的观察点 (未使用)

          // 将偏移量应用到胶囊体的旋转上，得到世界坐标系下的偏移
          const offsetWorld = idealOffset.clone();
          offsetWorld.applyQuaternion(capsule.quaternion); // 注意：这里用的是可视化胶囊体的旋转，可能与 playerCollider 不同步，但此例中两者旋转一致
          offsetWorld.add(pos); // 添加到目标位置上

          // 使用 lerp 平滑地将相机移动到理想位置
          camera.position.lerp(offsetWorld, 0.1);

          oControls.update(); // 更新轨道控制器
        } else {
          // 如果人物模型未加载，重置相机位置和控制器
          oControls.object.position.set(0, 2, 5);
          oControls.update(true); // 初始化
        }
      }

      function getForwardVector() {
        capsule.getWorldDirection(playerDirection);
        playerDirection.y = 0;
        playerDirection.normalize();

        return playerDirection;
      }

      function getSideVector() {
        capsule.getWorldDirection(playerDirection);
        playerDirection.y = 0;
        playerDirection.normalize();
        playerDirection.cross(capsule.up);

        return playerDirection;
      }

      // 动画切换函数
      function fadeToAction(newAction, duration = 0.2) {
        // 如果动作相同，不需要切换
        if (currentAction === newAction) return;

        // 记录之前的动作并更新当前动作
        const prevAction = currentAction;
        currentAction = newAction;

        // 获取对应的动作对象
        let prevActionObj = null;
        let nextActionObj = null;

        if (prevAction === "idle") {
          prevActionObj = idleAction;
        } else if (prevAction === "walk") {
          prevActionObj = walkAction;
        }

        if (newAction === "idle") {
          nextActionObj = idleAction;
        } else if (newAction === "walk") {
          nextActionObj = walkAction;
        }

        // 确保动作对象存在
        if (prevActionObj && nextActionObj) {
          // 设置权重为0，确保动画能够正确混合
          prevActionObj.enabled = true;
          nextActionObj.enabled = true;

          // 设置时间缩放，确保动画速度正确
          nextActionObj.timeScale = 1.0;

          // 淡出前一个动作
          prevActionObj.fadeOut(duration);

          // 重置并淡入新动作
          nextActionObj.reset();
          nextActionObj.setEffectiveWeight(1.0); // 确保权重设置正确
          nextActionObj.fadeIn(duration);
          nextActionObj.play();

          console.log("动画切换:", prevAction, "->", newAction);
        } else {
          console.error("动画切换失败:", prevAction, "->", newAction);
        }
      }

      function controls(deltaTime) {
        // 设置胶囊体旋转跟随相机旋转
        capsule.rotation.y = camera.rotation.y;

        // 如果有人物模型，也让人物模型跟随相机旋转
        if (peopleObj) {
          peopleObj.rotation.y = camera.rotation.y + Math.PI; // 增加 Math.PI 使其背对相机
        }

        // gives a bit of air control
        const speedDelta = deltaTime * (playerOnFloor ? 25 : 8);

        // 检测是否有移动输入
        let isMoving = false;

        if (keyStates["KeyW"]) {
          playerVelocity.add(getForwardVector().multiplyScalar(-speedDelta));
          isMoving = true;
        }

        if (keyStates["KeyS"]) {
          playerVelocity.add(getForwardVector().multiplyScalar(speedDelta));
          isMoving = true;
        }

        if (keyStates["KeyA"]) {
          playerVelocity.add(getSideVector().multiplyScalar(speedDelta));
          isMoving = true;
        }

        if (keyStates["KeyD"]) {
          playerVelocity.add(getSideVector().multiplyScalar(-speedDelta));
          isMoving = true;
        }

        // 根据移动状态切换动画
        if (peopleObj && mixer) {
          if (isMoving && playerOnFloor) {
            if (currentAction !== "walk") {
              fadeToAction("walk");
            }
          } else if (!isMoving && playerOnFloor) {
            if (currentAction !== "idle") {
              fadeToAction("idle");
            }
          }
        }

        if (playerOnFloor) {
          if (keyStates["Space"]) {
            playerVelocity.y = 8;
          }
        }
      }

      // 创建文字精灵
      var getTextCanvas = function (text) {
        let option = {
            fontFamily: "Arial",
            fontSize: 30,
            fontWeight: "bold",
            color: "#ffffff",
            actualFontSize: 0.08,
          },
          canvas,
          context,
          textWidth,
          texture,
          materialObj,
          spriteObj;
        canvas = document.createElement("canvas");
        context = canvas.getContext("2d");
        // 先设置字体大小后获取文本宽度
        context.font =
          option.fontWeight + " " + option.fontSize + "px " + option.fontFamily;
        textWidth = context.measureText(text).width;

        canvas.width = textWidth;
        canvas.height = option.fontSize;

        context.textAlign = "center";
        context.textBaseline = "middle";
        context.fillStyle = option.color;
        context.font =
          option.fontWeight + " " + option.fontSize + "px " + option.fontFamily;
        context.fillText(text, textWidth / 2, option.fontSize / 1.8);

        texture = new THREE.CanvasTexture(canvas);
        materialObj = new THREE.SpriteMaterial({
          map: texture,
        });
        spriteObj = new THREE.Sprite(materialObj);
        spriteObj.scale.set(
          (textWidth / option.fontSize) * option.actualFontSize,
          option.actualFontSize,
          option.actualFontSize
        );

        return spriteObj;
      };

      // 随机生成建筑物函数
      function generateRandomBuildings() {
        // 创建地面
        const groundGeometry = new THREE.PlaneGeometry(100, 100);
        const groundMaterial = new THREE.MeshStandardMaterial({
          color: 0x999999,
          roughness: 0.8,
          metalness: 0.2,
        });
        const ground = new THREE.Mesh(groundGeometry, groundMaterial);
        ground.rotation.x = -Math.PI / 2;
        ground.position.y = -0.1;
        ground.receiveShadow = true;
        scene.add(ground);

        // 创建建筑物组
        const buildingsGroup = new THREE.Group();

        // 生成多个随机建筑物
        const buildingCount = 30;
        const buildingColors = [
          0x8899aa, 0x7788aa, 0x6677aa, 0x5566aa, 0x4455aa,
        ];

        for (let i = 0; i < buildingCount; i++) {
          // 随机位置和大小
          const posX = (Math.random() - 0.5) * 80;
          const posZ = (Math.random() - 0.5) * 80;
          const width = 2 + Math.random() * 6;
          const height = 3 + Math.random() * 15;
          const depth = 2 + Math.random() * 6;

          // 创建建筑物
          const buildingGeometry = new THREE.BoxGeometry(width, height, depth);
          const buildingMaterial = new THREE.MeshStandardMaterial({
            color:
              buildingColors[Math.floor(Math.random() * buildingColors.length)],
            roughness: 0.7,
            metalness: 0.2,
          });

          const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
          building.position.set(posX, height / 2, posZ);
          building.castShadow = true;
          building.receiveShadow = true;

          buildingsGroup.add(building);
        }

        // 添加到场景
        scene.add(buildingsGroup);

        // 将生成的建筑物几何体添加到八叉树中，用于碰撞检测
        worldOctree.fromGraphNode(buildingsGroup);
        // 将地面也添加到八叉树中
        worldOctree.fromGraphNode(ground);

        return buildingsGroup;
      }

      // 生成建筑物并开始动画
      generateRandomBuildings();
      animate();

      let gloader = new GLTFLoader();
      gloader.load("./models/Xbot.glb", (result) => {
        peopleObj = result.scene;
        peopleObj.scale.set(0.7, 0.7, 0.7);
        peopleAnimations = result.animations;
        console.log(peopleAnimations);

        // 主人物名字
        let spriteText = getTextCanvas("唯众");
        spriteText.position.set(0, 1.95, 0);
        peopleObj.add(spriteText);

        const bbox = new THREE.Box3().setFromObject(peopleObj);

        // 获取包围盒的中心点
        const center = new THREE.Vector3();
        bbox.getCenter(center);

        // 将物体移动到中心点
        peopleObj.position.sub(center);

        // 组合对象添加到场景中
        scene.add(peopleObj);

        // 创建动画混合器
        mixer = new THREE.AnimationMixer(peopleObj);
        mixerArr.push(mixer);

        // 确保动画数组存在
        if (peopleAnimations && peopleAnimations.length > 0) {
          console.log("加载动画:", peopleAnimations.length, "个");

          // 输出所有动画名称，帮助调试
          peopleAnimations.forEach((clip, index) => {
            console.log(`动画 ${index}: ${clip.name}`);
          });

          // 设置动画动作 - 根据名称查找合适的动画
          // 尝试查找名称中包含idle和walk的动画
          let idleIndex = -1;
          let walkIndex = -1;

          peopleAnimations.forEach((clip, index) => {
            const name = clip.name.toLowerCase();
            if (name.includes("idle")) {
              idleIndex = index;
            } else if (name.includes("walk")) {
              walkIndex = index;
            }
          });

          // 如果没有找到匹配的名称，使用默认索引
          if (idleIndex === -1) idleIndex = 0; // 通常第一个动画是idle
          if (walkIndex === -1) walkIndex = 1; // 通常第二个动画是walk

          console.log(`使用动画索引: idle=${idleIndex}, walk=${walkIndex}`);

          // 设置动画动作
          idleAction = mixer.clipAction(peopleAnimations[idleIndex]);
          walkAction = mixer.clipAction(peopleAnimations[walkIndex]);

          // 确保动画正确初始化
          if (idleAction && walkAction) {
            // 设置循环模式和时间缩放
            idleAction.setLoop(THREE.LoopRepeat);
            walkAction.setLoop(THREE.LoopRepeat);

            // 确保动画权重和启用状态正确
            idleAction.enabled = true;
            walkAction.enabled = true;
            idleAction.setEffectiveWeight(1.0);
            walkAction.setEffectiveWeight(0.0);

            // 开始播放idle动画
            idleAction.play();
            currentAction = "idle";
            console.log("动画初始化成功");
          } else {
            console.error("动画初始化失败");
          }
        } else {
          console.error("动画数据不完整");
        }
      });

      function teleportPlayerIfOob() {
        // 检测玩家是否掉出世界边界 (Y 坐标过低)
        if (camera.position.y <= -25) {
          // 重置玩家碰撞体的位置和大小
          playerCollider.start.set(0, 0.35, 0);
          playerCollider.end.set(0, 1, 0);
          playerCollider.radius = 0.35;
          // 重置玩家速度
          playerVelocity.set(0, 0, 0);
          // 重置相机位置到角色后方
          const playerPos = playerCollider.end.clone();
          camera.position.set(playerPos.x, playerPos.y + 2, playerPos.z + 5);
          camera.lookAt(playerPos); // 让相机看向玩家
          oControls.target.copy(playerPos); // 更新控制器目标
          oControls.update();
        }
      }

      function animate() {
        // 只调用一次getDelta获取时间增量，避免多次调用导致时间不连续
        const deltaTime = Math.min(0.05, clock.getDelta()) / STEPS_PER_FRAME;

        // we look for collisions in substeps to mitigate the risk of
        // an object traversing another too quickly for detection.

        // 使用相同的deltaTime更新动画混合器
        for (let i = 0; i < mixerArr.length; i++) {
          mixerArr[i].update(deltaTime * STEPS_PER_FRAME);
        }

        for (let i = 0; i < STEPS_PER_FRAME; i++) {
          controls(deltaTime);

          updatePlayer(deltaTime);

          // updateSpheres(deltaTime);

          teleportPlayerIfOob();
        }

        // 确保每帧更新OrbitControls，使相机平滑跟随
        oControls.update();

        renderer.render(scene, camera);

        stats.update();

        requestAnimationFrame(animate);
      }
    </script>
  </body>
</html>
